//
// OpenTemplate
// Copyright (c) 2006
// by OpenArrow Software ( http://www.openarrow.com )
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions 
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Text;
using OpenArrow.Templating.Properties;
using System.Collections.Specialized;
using System.Reflection;
using System.ComponentModel;
using OpenArrow.Reporting;
using System.Security.Permissions;

namespace OpenArrow.Templating.Directives
{
    /// <summary>
    /// Utility class for building directive classes out of their string representations
    /// </summary>
    public static class DirectiveManager
    {
        private static Dictionary<string, Type> directiveTypes = new Dictionary<string, Type>();
        private static Dictionary<string, DirectivePropertyHelper> propertyHelpers = new Dictionary<string, DirectivePropertyHelper>();

        static DirectiveManager()
        {
            AddDirectiveType(typeof(TemplateDirective));
            AddDirectiveType(typeof(PropertyDirective));
            AddDirectiveType(typeof(ReferenceDirective));
            AddDirectiveType(typeof(ImportDirective));
        }

        /// <summary>
        /// Registers the specified type as a new directive type
        /// </summary>
        /// <param name="type">The type to register as a new directive type</param>
        /// <exception cref="System.InvalidCastException">The type does not implement the <see cref="IDirective"/> interface</exception>
        /// <exception cref="System.InvalidOperationException">The type does not have a <see cref="DirectiveAttribute"/> attribute</exception>
        /// <exception cref="ArgumentNullException"><paramref name="type"/> is <see langword="null"/></exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)")]
        public static void AddDirectiveType(Type type)
        {
            if(type == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "type"), "type");

            if (!typeof(IDirective).IsAssignableFrom(type))
                throw new InvalidCastException(Resources.Error_NotConvertibleToIDirective);
            
            string name = DirectiveAttribute.GetDirectiveName(type);
            if (name == null)
                throw new InvalidOperationException(Resources.Error_NoDirectiveAttribute);

            directiveTypes.Add(name, type);
        }

        /// <summary>
        /// Builds a Directive from the specified information
        /// </summary>
        /// <param name="name">The name of the directive to build</param>
        /// <param name="attributes">A <see cref="NameValueCollection"></see> containing the values to assign to the Directive properties</param>
        /// <param name="lineNo">The line number of the directive</param>
        /// <param name="problems">A collection of problems that this method should add to if there are errors or warnings</param>
        /// <param name="reporter">A progress reporter to report to</param>
        /// <returns>
        /// The directive represented by the specified name, with properties set. 
        /// Or <see langword="null" /> if there were errors in building the directive (check <paramref name="problems"/> for more info)
        /// </returns>
        /// <exception cref="ArgumentNullException"><paramref name="attributes"/> is null</exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)")]
        public static IDirective BuildDirective(string name, NameValueCollection attributes, int lineNo,
            ProblemCollection problems, IProgressReporter reporter)
        {
            if (attributes == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "attributes"), "attributes");
            if (reporter == null)
                reporter = new NullProgressReporter();

            // Check that the directive is registered
            if (!directiveTypes.ContainsKey(name))
            {
                problems.Add(Problem.Warning_DirectiveTypeNotRegistered(name, lineNo));
                return null;
            }

            // Create the directive
            IDirective directive = (IDirective)Activator.CreateInstance(directiveTypes[name]);

            // Fill the properties from the attributes
            foreach (string key in attributes)
            {
                using (reporter.StartTask(String.Format(Resources.Task_ParsingAttribute, key)))
                {
                    // Use a DirectivePropertyHelper to cache the info in case we need to load this directive again   
                    string propertyFullName = String.Format("{0}.{1}", name, key);
                    if (!propertyHelpers.ContainsKey(propertyFullName))
                        propertyHelpers.Add(propertyFullName, new DirectivePropertyHelper(directiveTypes[name], key));
                    propertyHelpers[propertyFullName].SetValue(directive, attributes[key], problems, lineNo);
                }
            }
            return directive;
        }

        private class DirectivePropertyHelper
        {
            private TypeConverter converter;
            private PropertyInfo property;
            private Type type;
            private bool isCached;
            private bool canUseConverter;
            private string propertyName;

            public DirectivePropertyHelper(Type type, string propertyName)
            {
                this.type = type;
                this.propertyName = propertyName;
            }

            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Convert.ChangeType(System.Object,System.Type)")]
            public void SetValue(object obj, string strValue, ProblemCollection problems, int lineNo)
            {
                if (!isCached)
                {
                    // Find the property
                    property = type.GetProperty(propertyName);
                    if (property == null)
                    {
                        problems.Add(Problem.Error_UnrecognizedAttribute(propertyName, lineNo));
                        return;
                    }

                    // Verify that it is Writable
                    if (!property.CanWrite)
                    {
                        problems.Add(Problem.Error_CannotMapAttributeToReadOnlyProperty(property.Name, lineNo));
                        return;
                    }

                    // Check for a type converter
                    converter = TypeDescriptor.GetProperties(type)[propertyName].Converter;
                    canUseConverter = ((converter != null) && (converter.CanConvertFrom(typeof(string))));
                    isCached = true;
                }

                object value = canUseConverter ? converter.ConvertFromString(strValue) : 
                    Convert.ChangeType(strValue, property.PropertyType);

                if (value == null)
                {
                    problems.Add(Problem.Error_FailedToConvertAttributeValue(property.PropertyType.FullName, lineNo));
                    return;
                }

                property.SetValue(obj, value, null);
            }
        }
    }
}
